/*****************************************************************************
 *   Copyright 2005 Tim A Wang
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 ****************************************************************************/
package com.jb.web.bean;

import com.jb.faces.component.UIComponent;
import com.jb.faces.context.FacesContext;
import com.jb.faces.el.AttributeEvaluator;
import com.jb.framework.Persistable;
import com.jb.framework.PersistentContext;
import com.jb.framework.PersistentDescriptor;
import com.jb.framework.PersistentManager;
import com.jb.framework.util.PropUtil;
import com.jb.web.application.RootContainer;
import com.jb.web.application.WebBeanz;
import com.jb.web.bean.inspector.DefaultPropertyEditor;
import com.jb.web.context.WebContext;

import java.beans.PropertyEditor;

import java.io.Serializable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.servlet.jsp.el.ELException;

import javax.swing.event.EventListenerList;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * This is the base UI component for jb framework.
 *
 * @author Tim Wang
 * @version 1.0.
 */
public class BaseComponent extends WebBeanz implements Serializable, 
                                                       ValueChangeListener, 
                                                       Persistable {
    static final long serialVersionUID = 123123L;
    private Log log = LogFactory.getLog(getClass().getName());

    //this 
    protected transient BaseComponent copiedFrom; //used for cloning; might not needed
    protected EventListenerList listenerList = new EventListenerList();
    protected transient boolean cloned = false;
    //    transient private FacesContext context;

    //pm that loads this object
    private PersistentManager persistentManager;
    protected FacesContext context;

    //seems not used, 
    private boolean chilrenSortted = false;
    private int sequence;
    private int maxSeq;
    //sequence in the children, controllable to the componnet user

    public void addFacet(String key, UIComponent comp) {
        this.getFacets().put(key, comp);
    }

    /**
     * Creates a new Beanz object.
     */
    public BaseComponent() {
        super();
    }

    /**
     * Returns true if this component can accept a child component
     *
     * @return boolean
     */
    public boolean getAllowsChildren() {
        return true;
    }
    /**
     * Returns max sequence.
     * @return max sequence in its children.
     */
    
    public int getMaxSeq() {
        if(maxSeq>0)
            return maxSeq;
        
        Iterator iter = getChildren().iterator();
        while(iter.hasNext()) {
            BaseComponent comp = (BaseComponent)iter.next();
            if(comp.getSequence()>maxSeq)
                maxSeq=comp.getSequence();
        }
        return maxSeq;
    }
    /**
     * Returns number of children.
     *
     * @return a number.
     */
    public int getChildCount() {
        return getChildren().size();
    }

    /**
     * Get a list of child component for a given class type, sorted with default
     * comparator. Testing uses instanceof method of the class passed in.
     *
     * @param newClass type of the components
     *
     * @return List of components.
     */
    public List getChildrenByType(Class newClass) {
        ArrayList v = new ArrayList();

        for (int i = 0; i < getChildren().size(); i++) {
            BaseComponent child = (BaseComponent)getChildren().get(i);
            if (newClass.isInstance(child)) {
                v.add(child);
            }
        }

        sortChildren(v, null);

        return v;
    }

    /**
     * Returns list of child components sorted by the comparator passed in.
     *
     * @param c comparator to sort.
     *
     * @return List of components.
     */
    public List getChildrenSorted(Comparator c) {
        ArrayList v = new ArrayList();

        for (int i = 0; i < getChildren().size(); i++) {
            BaseComponent child = (BaseComponent)getChildren().get(i);
            v.add(child);
        }

        sortChildren(v, c);

        return v;
    }

    /**
     * Sets client id. A client id is the id used by the client system, e.g. a browser.
     * This value is only used on the server side to locate a component
     *
     * @param newId a client id.
     */
    public void setClientId(String newId) {
        if ((newId != null) && !newId.equals(clientId)) {
            clientId = newId;
        }
    }

    String clientId;

    /**
     * Returns client id of this component.
     *
     * @return client id.
     */
    public String getClientId() {
        if (clientId == null) {
            clientId = getRoot().generateClientId(this.getClass());
        }
        return clientId;
    }

    /**
     * Returns client id per JSF specification.
     * @param ctx context container
     * @return client id.
     */
    public String getClientId(FacesContext ctx) {
        try {
            return (String)AttributeEvaluator.evaluate((WebContext)ctx, 
                                                                       this, 
                                                                       getClientId());
        } catch (ELException e) {
            if (log.isErrorEnabled())
                log.error(this, e);
            return getClientId();
        }
    }

    /**
     * Returns true if this component instance is cloned from a orginal.
     *
     * @return a boolean value.
     */
    public boolean isCloned() {
        return cloned;
    }

    /**
     * Returns child component by its position in its parent list. Note: it is not
     * by its sequence number.
     *
     * @param index position of the component
     *
     * @return child componnet if index is within the bound, otherwise null is returned.
     */
    public Object getComponentAt(int index) {
        if (getChildren().size() > index) {
            return getChildren().get(index);
        }
        return null;
    }

    /**
     * Returns component of a given id. This is a recursive call,
     * start from current component until find a match. Same as getDescendantById.
     * if not found, return null. The first match is returned.
     *
     * @param pId component id
     *
     * @return component of the given id.
     */
    public BaseComponent getComponentById(String pId) {
        if (this.getId().equals(pId)) {
            return this;
        }

        Iterator c = getFacetsAndChildren();

        while (c.hasNext()) {
            BaseComponent child = (BaseComponent)c.next();

            if (child.getId().equals(pId)) {
                return child;
            } else {
                BaseComponent grandChild = child.getComponentById(pId);

                if (grandChild != null) {
                    return grandChild;
                }
            }
        }

        return null;
    }

    /**
     * Returns child component by name. This is not a recursive call. If not found
     * returns null.
     *
     * @param name readable name
     *
     * @return DOCUMENT ME!
     */
    public Object getComponentByName(Object name) {
        Iterator c = getFacetsAndChildren();

        while (c.hasNext()) {
            BaseComponent child = (BaseComponent)c.next();

            if (child.getName().equals(name)) {
                return child;
            }
        }

        return null;
    }

    /**
     * Returns a list of child component types of this component.
     *
     * @return list of classes.
     */
    public List getComponentTypes() {
        Vector v = new Vector();

        for (int i = 0; i < getChildren().size(); i++) {
            BaseComponent child = (BaseComponent)getChildren().get(i);

            if (!v.contains(child.getClass())) {
                v.add(child.getClass());
            }
        }

        return v;
    }

    /**
     * Returns database descriptor for this component. The descriptor can be used
     * by persistent manager to manage storage for this component.
     *
     * @return PersistentDescriptor for this component.
     */
    public PersistentDescriptor getDBDescriptor() {
        return null;
    }

    /**
     * Sets default value when a new component is placed on the screen when designing
     * Component writer should implmenet this method to set the default behavior of
     * the component.
     *
     * @param ctx the runtime context.
     */
    public void setDefault(PersistentContext ctx) {
        setRendered(true);
    }

    /**
     * Returns ture if this component is a descendant of the component passed in.
     *
     * @param parent the parent component te be tested.
     *
     * @return ture if this component is the decendant of the parent.
     */
    public boolean isDescendant(BaseComponent parent) {
        if (getParent() == parent) {
            return true;
        } else if ((getParent() == null) || 
                   !(getParent() instanceof BaseComponent)) {
            return false;
        } else {
            return ((BaseComponent)getParent()).isDescendant(parent);
        }
    }

    /**
     * Returns decendant by its client id. This call recursively goes
     * through its children and return the first match. If not found, null is returnd.
     *
     * @param clientId client id to match
     *
     * @return UICompoent whose client id matches the parameter.
     */
    public BaseComponent getDescendantByClientId(String clientId) {
        if (clientId == null)
            return null;
        Iterator c = getFacetsAndChildren();

        while (c.hasNext()) {
            BaseComponent child = (BaseComponent)c.next();

            if (child instanceof UIComponentImpl) {
                BaseComponent uic = child;

                if (clientId.equals(uic.getClientId())) {
                    return uic;
                } else {
                    BaseComponent grandChild = 
                        uic.getDescendantByClientId(clientId);

                    if (grandChild != null) {
                        return grandChild;
                    }
                }
            }
        }

        return null;
    }

    /**
     * Returns a decendant for a given id. This is a recursive call, and null is returned
     * if not found
     *
     * @param id the Id of the component.
     *
     * @return BaseComponent found, null if not found.
     */
    public BaseComponent getDescendantById(String id) {
        Iterator c = getFacetsAndChildren();

        while (c.hasNext()) {
            BaseComponent child = (BaseComponent)c.next();

            if (child.getId().equals(id)) {
                return child;
            } else {
                BaseComponent grandChild = child.getDescendantById(id);

                if (grandChild != null) {
                    return grandChild;
                }
            }
        }
        return null;
    }

    /**
     * Returns a list of all descendants whose type are the same.
     *
     * @param classType class of the descendants.
     *
     * @return List of Classes.
     */
    public List getDescendants(Class classType) {
        ArrayList list = new ArrayList();
        Iterator c = getFacetsAndChildren();
        while (c.hasNext()) {
            UIComponentImpl comp = (UIComponentImpl)c.next();

            if (classType.isInstance(comp)) {
                list.add(comp);
            }

            List list1 = comp.getDescendants(classType);
            list.addAll(list1);
        }

        return list;
    }

    /**
     * Returns faces context, per JSF spcification.
     *
     * @return Faces context
     */
    public FacesContext getFacesContext() {
        return this.context;
    }

    /**
     * Returns list of children and facets
     * sorted by the comparator passed in.
     *
     * @param c comparator to sort.
     *
     * @return List of components.
     */
    public List getFacetsAndChildrenSorted(Comparator c) {
        ArrayList v = new ArrayList();
        Iterator iter = this.getFacetsAndChildren();

        while (iter.hasNext()) {
            BaseComponent child = (BaseComponent)iter.next();
            v.add(child);
        }

        sortChildren(v, c);

        return v;
    }

    /**
     * Returns a readable name for this component. By default, it returns its id.
     *
     * @return a readable name of the component.
     */
    public String getName() {
        return getId();
    }

    /**
     * Returns true is a property is published for this component. i.e.
     * in the static session, it has publish(class, type, propertyName);
     *
     * @param pPropertyName DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public boolean isPublished(String pPropertyName) {
        return DefaultPropertyEditor.isPublished(pPropertyName, this);
    }

    public String getQName() {
        return getRoot().getId() + ":" + getId();
    }

    /**
     * Returns root UI component of the UI tree this component belongs to.
     *
     * It usually is a page component.
     *
     * @return root ui component.
     */
    public BaseComponent getRoot() {
        UIComponent parent = getParent();

        if (parent == null) {
            return this;
        }

        if (!(parent instanceof BaseComponent)) {
            return this;
        }

        return ((BaseComponent)parent).getRoot();
    }

    /**
     * Sets sequence.
     *
     * @param sequence new sequence number.
     */
    public void setSequence(int sequence) {
        if (this.sequence != sequence) {
            this.sequence = sequence;
            
            if ((getParent() != null) && 
                getParent() instanceof BaseComponent) {
                BaseComponent com = (BaseComponent)getParent();
                com.chilrenSortted = false;
            }
        }
    }

    /**
     * Returns sequence number. This number is used to order the component in
     * its parent container. Renderers might use it for rendering purposes.
     *
     * @return sequence number.
     */
    public int getSequence() {
        return sequence;
    }

    /**
     * Add a child component
     *
     * @param component to be added
     *
     * @throws ChildTypeNotAllowedException if a container does not accept the child.
     */
    public void addChild(Persistable component) throws ChildTypeNotAllowedException {
        if (component == null || contains(component)) {
            return;
        }
        if(!(component instanceof BaseComponent)) {
            throw new ChildTypeNotAllowedException("Wrong object type for "+this.getClass().getName());
        }
        BaseComponent com = (BaseComponent)component;
        //give a chance to the child to deny 
        com.parentAcceptable(this);
        BaseComponent parent = (BaseComponent)com.getParent();
        if(parent!=null)
            parent.removeChild(com);
        getChildren().add(com);
        
        if(com.getSequence() > maxSeq)
            maxSeq=com.getSequence();
    }

    /**
     * Register a value change lisnter. Value change listners are invoked when
     * when fireValueChanged is called.
     *
     * @param valueLister
     */
    public void addValueChangeListener(ValueChangeListener valueLister) {
        listenerList.add(ValueChangeListener.class, valueLister);
    }

    /**
     * Assigns a new component to this component.
     *
     * @param component the component to be assigned.
     */
    public void assign(BaseComponent component) {
        PropUtil.assignProperties(component, this);
    }

    /**
     * Removes all the children from this component. The components might not
     * deleted in any other sence. All the children are de referenced from this
     * component.
     */
    public void clearChildren() {
        Iterator c = getChildren().iterator();

        while (c.hasNext()) {
            BaseComponent child = (BaseComponent)c.next();
            child.clearChildren();
        }

        getChildren().clear();
    }

    /**
     * Clones a component, the instances are identical, including
     * componentid This is used mainly for caching at session level.
     *
     * @param ctx the runtime context.
     * @param cloneId the flag indicates if a id should be generated for the cloned
     * component.
     * @param cloneClientId indicate if a client id is needed for the cloned
     * component.
     *
     * @return created componented
     */
    public BaseComponent clone(PersistentContext ctx, boolean cloneId, 
                               boolean cloneClientId) {

        BaseComponent component = null;

        try {
            component = (BaseComponent)getClass().newInstance();
        } catch (Exception e) {
            if (log.isErrorEnabled())
                log.error(this, e);
            return null;
        }

        //new id
        String tempId = component.getId();

        //String oldClientId = component.getId();
        component.cloned = true;

        Iterator iter = getChildren().iterator();

        while (iter.hasNext()) {
            Object uiChild = iter.next();

            if (uiChild instanceof BaseComponent) {
                BaseComponent ui = (BaseComponent)uiChild;
                component.addChild((BaseComponent)ui.clone(ctx, cloneId, cloneClientId));
            }
        }

        iter = getFacets().keySet().iterator();

        while (iter.hasNext()) {
            String facetKey = (String)iter.next();
            BaseComponent ui = (BaseComponent)getFacets().get(facetKey);

            component.addFacet(facetKey, 
                               (BaseComponent)ui.clone(ctx, cloneId, cloneClientId));

        }

        component.changeToken = changeToken;
        component.postCreate(ctx);
        component.assign(this); //override id as well
        component.copiedFrom = this;
        component.addValueChangeListener(this);

        if (cloneId) {
            component.cloned = false;
            component.copiedFrom = null;
            component.setId(tempId); //old id
        }

        if (cloneClientId) {
            component.setClientId(tempId);
        }

        return component;
    }

    /**
     * Return true if the component is a child of this component. Testing on
     * reference as well as qName.
     *
     * @param component to be tested
     *
     * @return a boolean value.
     */
    public boolean contains(Persistable component) {
        for (int i = 0; i < getChildren().size(); i++) {
            BaseComponent comp = (BaseComponent)getChildren().get(i);

            if (comp == component) {
                return true;
            }

            if (comp.getQName().equals(((BaseComponent)component).getQName()) && 
                (comp.getClass() == component.getClass())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Deletes to the persistent storage.
     *
     * @param ctx context container
     */
    public void delete(PersistentContext ctx) {
        RootContainer.remove(ctx, this);

        Iterator iter = getFacetsAndChildren();

        while (iter.hasNext()) {
            BaseComponent wb = (BaseComponent)iter.next();
            wb.delete(ctx);
        }
    }

    /**
     * Fires value changed event for this component.
     *
     * @param event the event to fire.
     */
    public void fireValueChanged(ValueChangeEvent event) {
        for (int i = 0; i < listenerList.getListenerCount(); i++) {
            ValueChangeListener listener = 
                (ValueChangeListener)listenerList.getListenerList()[(i)];
            listener.valueChanged(event);
        }
    }

    /**
     * Called by session when a personalized version is created. Component should override this
     * method to load personalization setting for this component
     *
     * @param ctx
     */
    private void loadPersonalization(WebContext ctx) {
        Iterator iter = getFacetsAndChildren();

        while (iter.hasNext()) {
            BaseComponent child = (BaseComponent)iter.next();
            child.loadPersonalization(ctx);
        }
    }

    //this is called by the view handler after restored

    /**
     * Called when the view is restored. This method must be called to bind the
     * context to the current tree.
     * @param ctx to be bound to the tree.
     */
    public void onRestored(FacesContext ctx) {
        this.context = ctx;
        Iterator iter = getFacetsAndChildren();
        while (iter.hasNext()) {
            BaseComponent child = (BaseComponent)iter.next();
            child.onRestored(context);
        }
    }

    /**
     * Lifecycle method called when the component is created.
     *
     * @param ctx runtime context.
     */
    public void postCreate(PersistentContext ctx) {
    }

    /**
     * Publish an attribute. A published property can be seen in the property
     * inspector at design time. An attribute can only be published if it is a
     * bean property (with public setter/getter).
     *
     * @param pClass Class whose property is to be published.
     * @param pType category of the property, not used.
     * @param pPropertyName name of the property.
     */
    public static void publish(Class pClass, String pType, 
                               String pPropertyName) {
        DefaultPropertyEditor.publish(pClass, pType, pPropertyName);
    }

    /**
     * Publish an attribute and associate it with a property editor.
     *
     * A published attribute can be seen in the property
     * inspector at design time. An attribute can only be published if it is a
     * bean property (with public setter/getter).
     *
     * @param pClass Class whose property is to be published.
     * @param Type category of the property, not used.
     * @param pPropertyName name of the property.
     * @param pe PropertyEditor for the property.
     */
    public static void publish(Class pClass, String Type, String pPropertyName, 
                               PropertyEditor pe) {
        DefaultPropertyEditor.publish(pClass, Type, pPropertyName, pe);
    }

    static Map wizards = new HashMap();

    public static void registerWizard(Class objectClass, Class wizardClass) {
        wizards.put(objectClass, wizardClass);
    }

    public static Object getWizard(Class objectClass) {
        return wizards.get(objectClass);
    }

    public void setPersistentManager(PersistentManager persistentManager) {
        this.persistentManager = persistentManager;
    }

    public PersistentManager getPersistentManager() {
        return persistentManager;
    }

    /**
     * Remove a child at given position.
     *
     * @param index where the child is removed.
     */
    public void removeChild(int index) {
        getChildren().remove(index);
    }

    /**
     * Remove child component.
     *
     * @param component to be removed.
     */
    public void removeChild(BaseComponent component) {
        getChildren().remove(component);
    }

    /**
     * Saves to the persistent storage.
     *
     * @param ctx context container
     */
    public void save(PersistentContext ctx) {
        RootContainer.save(ctx, this);
    }

    /**
     * Returns component id.
     *
     * @return component id.
     */
    public String toString() {
        return getId();
    }

    //default value change listener  

    /**
     * Implements ValueChangeListener. When a component is copied from an original, it is added
     * as a listener to the orginal, so if orginal changes its value, the copied one gets notified.
     *
     * @param event change event.
     */
    public void valueChanged(ValueChangeEvent event) {
        if (this.copiedFrom != null) {
            this.assign(copiedFrom);
        }
    }

    /**
     * Get a list of child component for a given class type, sorted with default
     * comparator. Testing uses newClass==child.getClass().
     *
     * @param newClass type of the components
     *
     * @return List of components.
     */
    protected List getChildrenByClass(Class newClass) {
        ArrayList v = new ArrayList();

        for (int i = 0; i < getChildren().size(); i++) {
            BaseComponent child = (BaseComponent)getChildren().get(i);

            if (newClass == child.getClass()) {
                v.add(child);
            }
        }

        sortChildren(v, null);

        return v;
    }

    protected BaseComponent getClonedFrom() {
        return copiedFrom;
    }

    /**
     * Merges a component with this component.
     *
     * @param ctx the runtime context.
     * @param component the component to merge with.
     */
    protected void merge(WebContext ctx, BaseComponent component) {
        
        //no-op
    }

    /**
     * Test if a component can be the of this component as a child. If not
     * the child should throw ChildTypeNotAllowedException
     * @param parent the component to add this as a child.
     * @throws ChildTypeNotAllowedException throw if the parent is not acceptable.
     */
    protected void parentAcceptable(BaseComponent parent) throws ChildTypeNotAllowedException {
    }

    /**
     * Restores the oringinal setting from where this component is cloned from
     *
     * @param ctx the runtime context.
     */
    protected void restoreDefault(WebContext ctx) {
        if (getClonedFrom() != null) {
            merge(ctx, getClonedFrom());
        }

        Iterator iter = getFacetsAndChildren();

        while (iter.hasNext()) {
            BaseComponent bc = (BaseComponent)iter.next();
            bc.restoreDefault(ctx);
        }
    }

    /**
     * Sorts a list, Collections.sort is used to perform sorting.
     *
     * @param v list to be sorted.
     * @param c a comparator for sorting. If null is passed, sequence number
     * is used for sorting.
     */
    protected void sortChildren(List v, Comparator c) {
        if (chilrenSortted) {
            return;
        }
        Collections.sort(v, (c != null) ? c : new Comparator() {
                    public int compare(Object a, Object b) {
                        BaseComponent comp1 = (BaseComponent)a;
                        BaseComponent comp2 = (BaseComponent)b;
                        Integer i1 = new Integer(comp1.getSequence());
                        Integer i2 = new Integer(comp2.getSequence());
                        int i = i1.compareTo(i2);

                        return i;
                    }
                });
        //chilrenSortted=true;
    }

    /**
     * Synchs up with orginal. This calls assign method, passing the orginal component.
     * The assign method will decide the policies as how to synch up with the orginal.
     *
     * Component writers can also override this method to alter its behavior though it is
     * highly discouraged to do so.
     */
    protected void synch() {
        if (copiedFrom != null) {
            assign(copiedFrom);
        }

        Iterator iter = this.getFacetsAndChildren();

        while (iter.hasNext()) {
            BaseComponent bc = (BaseComponent)iter.next();
            bc.synch();
        }
    }

    private String generateClientId(Class clazz) {
        return com.jb.web.model.GUID.next(clazz);
    }
}

//
